#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  Eizelle - fertility awareness calendar for the GNOME desktop
#
#  Copyright (c) 2014 Jonathon Conte
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

import gettext
import json
import pickle
import sys
from os import urandom
from base64 import b64encode, b64decode

from gi.repository import Gtk, GLib, Gio, Gdk, GObject
try:
	from Crypto.Cipher import AES
	from Crypto.Hash import SHA256
except ImportError:
	AES = None
	SHA256 = None


PROGRAM_NAME="Eizelle"
VERSION_NUMBER="PROGRAM_VERSION"
COPYRIGHT="Copyright (c) 2014 Jonathon Conte"
PROJECT_WEBSITE="https://code.google.com/p/eizelle/"
MAX_SUPPORTED_ENCRYPTION_VERSION=0

gettext.bindtextdomain(PROGRAM_NAME.lower(), "/usr/share/locale")
gettext.textdomain(PROGRAM_NAME.lower())
_ = gettext.gettext

color_data = [("".join(("<b>", _("Menstrual Cycle"), "</b>")), None),
              (_('Began'), 'cycle-previous'),
              (_('Estimated Start'), 'cycle-estimated'),
              ("".join(("<b>", _("Conception"), "</b>")), None),
              (_('Occurred'), 'conception'),
              (_('Low Chance'), 'conception-low-chance'),
              (_('High Chance'), 'conception-high-chance'),
              (_('Estimated Birth'), 'birth-estimated'),
              ("".join(("<b>", _("Birth Control"), "</b>")), None),
              (_('Took 1st Tablet'), 'tablet-first'),
              (_('Next 1st Tablet'), 'tablet-next-first'),
              (_('Placebo (or Pause)'), 'tablet-pause')]


class AboutWindow(Gtk.AboutDialog):
	"""
	Window to display the program name, version number, copyright,
	and website.
	"""
	def __init__(self, parent=None):
		"""
		parent: GtkWindow instance
		"""
		Gtk.AboutDialog.__init__(self)
		self.set_transient_for(parent)
		self.set_logo_icon_name(PROGRAM_NAME.lower())
		self.set_comments(_("Fertility Awareness Calendar for GNOME"))
		self.set_version(" ".join((_("Version"), VERSION_NUMBER)))
		self.set_copyright(COPYRIGHT)
		self.set_license_type(Gtk.License.GPL_3_0)
		self.set_website(PROJECT_WEBSITE)


class Application(Gtk.Application):
	"""
	Brings up MainWindow.
	"""
	def __init__(self):
		Gtk.Application.__init__(
			self,
			application_id="".join(("org.gnome.", PROGRAM_NAME.lower())))
		self.connect("activate", self.activate)
		GLib.set_prgname(PROGRAM_NAME)

	def activate(self, application):
		"""
		Special method called at startup by Gtk.Application.run()
		"""
		self.settings = Gio.Settings.new(
			"".join(("org.gnome.", PROGRAM_NAME.lower())))
		self.color_settings = Gio.Settings.new(
			"".join(("org.gnome.", PROGRAM_NAME.lower(), ".colors")))

		self.journal = Journal(
			cycle_lower_limit=self.settings.get_int("menstrual-cycle-lower-limit"),
			cycle_upper_limit=self.settings.get_int("menstrual-cycle-upper-limit"))

		main_window = MainWindow(self.journal, self.settings, self.color_settings)
		application.add_window(main_window)
		main_window.show()

		try:
			self.journal.load_from_file()
		except EizelleError as error:
			error_dialog = ErrorDialog(
				main_window,
				error.long_message,
				error.short_message)
			application.add_window(error_dialog)
			error_dialog.show_all()
			error_dialog.run()
			application.quit()
			return

		# Display an error message and quit if the journal is encrypted
		# but there is no crypto support installed.
		if (self.journal.is_locked() is True and
				self.journal.has_crypto_support() is False):
			error_dialog = ErrorDialog(
				main_window,
				_("The PyCrypto library must be installed in order to open the journal."))
			application.add_window(error_dialog)
			error_dialog.show_all()
			error_dialog.run()
			application.quit()
			return

		# Prompt user for password if journal data is encrypted.
		while self.journal.is_locked():
			password_dialog = PasswordDialog(
				main_window,
				_("Password Required"),
				_("Quit"),
				_("Unlock"))
			application.add_window(password_dialog)
			password_dialog.show_all()
			response = password_dialog.run()
			if response == Gtk.ResponseType.OK:
				self.journal.set_password(password_dialog.get_password())
				password_dialog.destroy()
			else:
				password_dialog.destroy()
				application.quit()
				break

		self.settings.connect(
			"changed::menstrual-cycle-lower-limit",
			self.settings_changed)
		self.settings.connect(
			"changed::menstrual-cycle-upper-limit",
			self.settings_changed)

	def settings_changed(self, settings, key):
		"""
		Callback function for GSettings.
		"""
		if key == "menstrual-cycle-lower-limit":
			self.journal.set_cycle_lower_limit(settings.get_int(key))

		elif key == "menstrual-cycle-upper-limit":
			self.journal.set_cycle_upper_limit(settings.get_int(key))


class Calendar(Gtk.Frame):
	"""
	Custom calendar widget used by CalendarView for each calendar
	displayed.

	Calendar.get_color() is where all of the predictions are calculated
	based on the existing journal entries in the journal.
	"""
	def __init__(
		self,
		birth_control_duration=None,
		birth_control_inactive_days=None,
		calendar_view=None,
		colors={},
		journal=None,
		method=None,
		month=None,
		year=None,
		shadow_type=Gtk.ShadowType.IN):
		"""
		birth_control_duration: integer
		birth_control_inactive_days: integer
		calendar_view: CalendarView instance
		colors:
		journal: Journal instance
		method: integer
		month: integer month in the range 1-12
		year: integer year in the range 1-65535
		shadow_type: Gtk.ShadowType
		"""
		Gtk.Frame.__init__(self)
		self.event_box = Gtk.EventBox(visible_window=True)
		self.add(self.event_box)
		self.calendar_view = calendar_view
		self.journal = journal
		self._colors = colors
		self._method = method

		# Number of days in a cycle of birth control pills. This is
		# often 28 but sometimes longer (e.g. 91 or 365) in the case of
		# extended cycle birth control.
		self._birth_control_total_duration = birth_control_duration

		# Number of non-hormonal (inactive/placebo) pills or number of
		# days to skip hormonal (active) pills. Typically this number
		# is 7 or 4. (It should be 0 in the case of continuous use).
		self._birth_control_inactive_days = birth_control_inactive_days

		# This is the color for the Calendar background. The same color
		# is used as the default background for each CalendarDay. The
		# default value is white.
		bg_color = Gdk.RGBA()
		result = bg_color.parse(self._colors["background"])
		#self.event_box.override_background_color(Gtk.StateFlags.NORMAL, bg_color)

		self.month_name_label = Gtk.Label.new("")
		self.month_name_label.set_halign(Gtk.Align.START)
		self.year_label = Gtk.Label.new("")
		self.year_label.set_halign(Gtk.Align.END)

		now = GLib.DateTime.new_now_local()
		self.year = year if year else now.get_year()
		self.month = month if month else now.get_month()

		# Create a 7 x 7 grid. The first row is used for the names of
		# the days of the week. The remaining six rows are used for the
		# days in month.
		self.grid = Gtk.Grid(column_spacing=6, row_spacing=6, column_homogeneous=True, row_homogeneous=True)

		# set weekday names header
		for day_number in range(7):
			name_of_day = now.add_days(day_number).format("%a")
			label = Gtk.Label.new(name_of_day)
			self.grid.attach(label, now.add_days(day_number).get_day_of_week() % 7, 0, 1, 1)

		self.calendar_day_widgets = []
		for row in range(6):
			for column in range(7):
				calendar_day = CalendarDay(self)
				calendar_day.connect("button-press-event", self.day_clicked)
				self.calendar_day_widgets.append(calendar_day)
				self.grid.attach(calendar_day, column, row + 1, 1, 1)

		self.set_month_year(self.month, self.year)

		vbox = Gtk.Box(
			orientation=Gtk.Orientation.VERTICAL,
			spacing=12,
			border_width=12)
		self.event_box.add(vbox)
		header=Gtk.Box(spacing=12)
		header.pack_start(self.month_name_label, True, True, 0)
		header.pack_start(self.year_label, True, True, 0)
		vbox.pack_start(header, False, True, 0)

		vbox.pack_start(self.grid, True, True, 0)

	def day_clicked(self, calendar_day, event):
		"""
		Callback for clicks on each CalendarDay.

		Returns: True
		"""
		label = calendar_day.get_child()
		day_of_month = label.get_text()
		calendar = self

		if event.button == 3 and day_of_month != "":
			self.day = int(day_of_month)
			self.popup = CalendarPopup(calendar, calendar_day)
			self.popup.popup_for_device(
				event.device,
				None,
				None,
				None,
				None,
				event.button,
				event.time)
		return True


	def get_color(self, day, month, year):
		"""
		This method determines what color to display for each
		CalendarDay instance displayed on Calendar. The color chosen is
		based on information from the journal such as existing journal
		entries and journal entry statistics. This information is
		combined with various formulas to make predictions about things
		like when chances of fertility are high or low and on which day
		birth is estimated following conception.

		It also tracks birth control pill usage to offer reminders about
		when to start the next prescription.

		Returns: Gdk.RGBA instance
		"""
		last_cycle_entry = self.journal.get_last_entry(JournalEntryTypeKnown.CYCLE)
		last_conception_entry = self.journal.get_last_entry(JournalEntryTypeKnown.CONCEPTION)
		last_tablet_entry = self.journal.get_last_entry(JournalEntryTypeKnown.TABLET)


		# This is the default color to use for each CalendarDay. If no
		# other rule matches, this method will return this color. The
		# value is white by default.
		bg_color = Gdk.RGBA()
		result = bg_color.parse(self._colors["background"])

		# Dates with at least one journal entry always have a color
		# displayed. If multiple journal entries exist for the same
		# date, the type with the highest priority will be shown since
		# CalendarDay is limited to show a single color at a time.

		# conception - highest priority
		if self.journal.get_journal_entry(year, month, day, JournalEntryTypeKnown.CONCEPTION):
			color = Gdk.RGBA()
			result = color.parse(self._colors["conception"])
			return color

		# tablet - medium priority
		if self.journal.get_journal_entry(year, month, day, JournalEntryTypeKnown.TABLET):
			color = Gdk.RGBA()
			result = color.parse(self._colors["tablet-first"])
			return color

		# start of new cycle - lowest priority
		if self.journal.get_journal_entry(year, month, day, JournalEntryTypeKnown.CYCLE):
			color = Gdk.RGBA()
			result = color.parse(self._colors["cycle-previous"])
			return color

		# PREDICTIONS
		# The start of future menstrual cycles, chance of fertility, and
		# delivery date are based on several calculations that might
		# flucuate based on new journal entries. For this reason, only
		# dates following the last journal entry are considered.

		# Pregnancy/Estimated Birth
		# If a conception entry was made, do not make future predictions
		# about fertility until a new menstrual cycle or birth control
		# has been recorded. If a new menstrual cycle is entered after
		# the conception entry but before the estimated due date, do not
		# display the due date.
		if last_conception_entry is not None:
			last_conception_date = last_conception_entry.get_date()
			elapsed_time = last_conception_date.days_between(new_date(day, month, year))
			if (last_cycle_entry is None or (last_cycle_entry.get_date().days_between(new_date(day, month, year)) >= elapsed_time)) and \
			(last_tablet_entry is None or (last_tablet_entry.get_date().days_between(new_date(day, month, year)) >= elapsed_time)):

				# todo: user defined 38/40 weeks to determine due date
				weeks_in_pregnancy = 38
				if elapsed_time < (weeks_in_pregnancy * 7):
					return bg_color
				if elapsed_time == (weeks_in_pregnancy * 7):
					color = Gdk.RGBA()
					result = color.parse(self._colors["birth-estimated"])
					return color
				# Do not predict fertility days following conception
				# until a new menstrual cycle has been recorded.
				if last_cycle_entry:
					if last_cycle_entry.get_date().days_between(last_conception_entry.get_date()) >= 0:
						return bg_color

		# Birth Control
		if last_tablet_entry is not None:
			last_tablet_date = last_tablet_entry.get_date()
			elapsed_time = last_tablet_date.days_between(new_date(day, month, year))
			if ((last_cycle_entry is None) or
				(last_cycle_entry.get_date().days_between(new_date(day, month, year)) >= elapsed_time)):

				if elapsed_time < 0:
					return bg_color
				# day to take placebo (or skip)
				if (elapsed_time >= (self._birth_control_total_duration - self._birth_control_inactive_days) and
						elapsed_time < self._birth_control_total_duration):
					color = Gdk.RGBA()
					result = color.parse(self._colors["tablet-pause"])
					return color

				# start of next birth control cycle
				if elapsed_time == self._birth_control_total_duration:
					color = Gdk.RGBA()
					result = color.parse(self._colors["tablet-next-first"])
					return color
				if (elapsed_time < (self._birth_control_total_duration - self._birth_control_inactive_days) or
						elapsed_time > self._birth_control_total_duration):
					return bg_color

		# Only color dates following the last recorded menstrual cycle.
		if self.journal.get_average_cycle_length() is not None:
			last_cycle_date = last_cycle_entry.get_date()
			elapsed_time = last_cycle_date.days_between(new_date(day, month, year))
			if (elapsed_time > 0) and (self.journal.get_total_entries(JournalEntryTypeKnown.CYCLE) > 1):
				avg_length = self.journal.get_average_cycle_length()

				# apply fertility awareness method rules
				if self._method == FertilityAwarenessMethod.STANDARD_DAYS:
					first_fertile_day = 8
					last_fertile_day = 19
				elif self._method == FertilityAwarenessMethod.RHYTHM:
					first_fertile_day = self.journal.get_shortest_cycle_length() - 18
					last_fertile_day = self.journal.get_longest_cycle_length() - 11
				else:
					raise EizelleError("Invalid type of FertilityAwarenessMethod.")

				# predicted start of next cycle
				if elapsed_time % avg_length == 0:
					color = Gdk.RGBA()
					result = color.parse(self._colors["cycle-estimated"])
					return color

				# high chance of conception
				if ((elapsed_time % avg_length >= (first_fertile_day - 1)) and
						(elapsed_time % avg_length <= (last_fertile_day - 1))):
					color = Gdk.RGBA()
					result = color.parse(self._colors["conception-high-chance"])
					return color

				# low chance of conception
				else:
					color = Gdk.RGBA()
					result = color.parse(self._colors["conception-low-chance"])
					return color

		# Date is earlier than any recorded journal entries or there are
		# not enough statistics available to make any estimates.
		return bg_color

	def get_ymd(self):
		"""
		Returns a date represented as a tuple of three integers in the
		form (year, month, day). The year and month are those that are
		currently displayed by the calendar.

		day is set from the Calendar.day_clicked callback function when
		a CalendarDay is clicked and it is part of the current month.

		Returns: tuple
		"""
		return self.year, self.month, self.day

	def refresh(self):
		"""
		Set the color for each CalendarDay instance on the calendar.
		"""
		# This is the color for the Calendar background. The same color
		# is used as the default background for each CalendarDay. The
		# default value is white.
		bg_color = Gdk.RGBA()
		result = bg_color.parse(self._colors["background"])

		self.event_box.override_background_color(
			Gtk.StateFlags.NORMAL,
			bg_color)

		# determine day of week for first day of the month
		start_of_month = GLib.DateTime.new_local(self.year, self.month, 1, 0, 0, 0)
		offset = start_of_month.get_day_of_week() % 7

		# determine length of month by subtracting one day from the
		# first day of the following month
		if self.month == 12:
			following_month = GLib.DateTime.new_local(self.year + 1, 1, 1, 0, 0, 0)
		else:
			following_month = GLib.DateTime.new_local(self.year, (self.month + 1), 1, 0, 0, 0)

		length_of_month = following_month.add_days(-1).get_day_of_month()

		# blank days at start of calendar
		for widget_number in range(offset):
			self.calendar_day_widgets[widget_number].get_child().set_text("")
			self.calendar_day_widgets[widget_number].override_background_color(Gtk.StateFlags.NORMAL, bg_color)

		# calendar days of the current month
		for widget_number in range(length_of_month):
			day_of_month = widget_number + 1
			if self.journal.get_journal_entry(self.year, self.month, day_of_month,JournalEntryTypeKnown.NOTE):
				self.calendar_day_widgets[widget_number + offset].get_child().set_markup("".join(("<b><u>", str(day_of_month), "</u></b>")))
			else:
				self.calendar_day_widgets[widget_number + offset].get_child().set_text(str(day_of_month))
			self.calendar_day_widgets[widget_number + offset].override_background_color(Gtk.StateFlags.NORMAL,
																	self.get_color(day_of_month, self.month, self.year))
		# blank days at end of calendar
		for widget_number in range(42 - (offset + length_of_month)):
			self.calendar_day_widgets[widget_number + offset + length_of_month].get_child().set_text("")
			self.calendar_day_widgets[widget_number + offset + length_of_month].override_background_color(Gtk.StateFlags.NORMAL, bg_color)

	def set_birth_control_duration(self, days):
		"""
		Set the duration in days of a birth control prescription.

		days: integer
		"""
		assert isinstance(days, int)
		self._birth_control_total_duration = days
		self.refresh()

	def set_birth_control_inactive_days(self, days):
		"""
		Set the number of days in a birth control prescription that
		inactive pills are taken.

		days: integer
		"""
		assert isinstance(days, int)
		self._birth_control_inactive_days = days
		self.refresh()

	def set_color(self, name, color):
		"""
		Set the value used for each color.
		"""
		self._colors[name] = color
		self.refresh()

	def set_fertility_awareness_method(self, method):
		"""
		Set the fertility awareness method used for determining
		whether the chance of fertility is high or low.

		Standard Days = 0
		Rhythm = 1

		method: integer
		"""
		self._method = method
		self.refresh()

	def set_month_year(self, month, year):
		"""
		populate month name, year, and day of month labels on calendar

		year: integer year in the range 1-65535
		month: 1-12
		"""
		self.year = year
		self.month = month
		self.day = None

		self.month_name_label.set_text(
			GLib.DateTime.new_local(
				self.year, self.month, 1, 0, 0, 0).format("%B"))
		self.year_label.set_text(str(self.year))

		self.refresh()


class CalendarDay(Gtk.EventBox):
	"""
	Widget used for the day rows and columns that make up each
	Calendar instance.
	"""
	def __init__(self, calendar):
		"""
		calendar: Calendar instance
		"""
		Gtk.EventBox.__init__(self, visible_window=True)
		self.calendar = calendar
		self.label = Gtk.Label.new("")
		self.label.set_hexpand(True)
		self.label.set_vexpand(True)
		self.add(self.label)

	def get_ymd(self):
		"""
		Returns the date of the calendar day represented by a tuple
		containing three integers in the form (year, month, day).

		Returns: tuple
		"""
		year, month, day = self.calendar.get_ymd()
		return year, month, day


class CalendarKey(Gtk.Box):
	"""
	Widget used by CalendarView to display a key of the colors with
	the names of the information they represent. This widget is also
	used by PreferencesWindow.
	"""
	def __init__(self, color_settings):
		"""
		color_settings: GSettings instance
		"""
		Gtk.Box.__init__(self)
		self.color_settings = color_settings

		grid = Gtk.Grid(column_spacing=12, row_spacing=12)
		self.pack_start(grid, True, True, 0)
		for number in range(len(color_data)):
			label_name, color_settings_key = color_data[number]
			if color_settings_key:
				color_settings_value = self.color_settings.get_string(color_settings_key)

				button = Gtk.ColorButton()
				# set_margin_start requires GTK+ 3.12
				try:
					button.set_margin_start(18)
				except AttributeError:
					button.set_margin_left(18)
				button.connect("color-set", self.button_color_changed, color_settings_key)
				self.color_settings.connect("".join(("changed::", color_settings_key)), self.color_settings_changed, button)
				self.color_settings_changed(self.color_settings, color_settings_key, button)
				grid.attach(button, 0, number, 1, 1)

				label = Gtk.Label.new(label_name)
				label.set_halign(Gtk.Align.START)
				grid.attach(label, 1, number, 1, 1)
			else:
				label = Gtk.Label.new(label_name)
				label.set_use_markup(True)
				label.set_halign(Gtk.Align.START)
				grid.attach(label, 0, number, 2, 1)

		# do not call show on self so that parent can decide to
		# show or hide CalendarKey
		grid.show_all()

	def button_color_changed(self, button, color_setting_key):
		"""
		Callback function for each color button.
		"""
		color_setting_value = button.get_rgba().to_string()
		self.color_settings.set_string(color_setting_key, color_setting_value)

	def color_settings_changed(self, color_settings, color_settings_key, button):
		"""
		Callback function for GSettings.
		"""
		color_settings_value = self.color_settings.get_string(color_settings_key)
		color = Gdk.RGBA()
		result = color.parse(color_settings_value)
		if result:
			button.set_rgba(color)


class CalendarNavigation(Gtk.Box):
	"""
	Widget used to navigate forward and backward on the CalendarView.
	"""
	def __init__(self, calendar_view):
		"""
		calendar_view: CalendarView instance
		"""
		Gtk.Box.__init__(self, homogeneous=False, spacing=6)
		hbox = Gtk.Box(homogeneous=False, spacing=0)
		Gtk.StyleContext.add_class(hbox.get_style_context(), "linked")
		#home_button = Gtk.Button.new_from_icon_name("gtk-home", Gtk.IconSize.BUTTON)
		home_button = Gtk.Button.new_with_label(_("Today"))
		home_button.connect("clicked", self.clicked, calendar_view.current)

		back_button = Gtk.Button.new()
		back_button.add(Gtk.Arrow(arrow_type=Gtk.ArrowType.LEFT, shadow_type=Gtk.ShadowType.NONE))
		back_button.connect("clicked", self.clicked, calendar_view.backward)
		hbox.pack_start(back_button, False, False, 0)

		forward_button = Gtk.Button.new()
		forward_button.add(Gtk.Arrow(arrow_type=Gtk.ArrowType.RIGHT, shadow_type=Gtk.ShadowType.NONE))
		forward_button.connect("clicked", self.clicked, calendar_view.forward)
		hbox.pack_start(forward_button, False, False, 0)

		self.pack_start(home_button, False, False, 0)
		self.pack_start(hbox, False, False, 0)

	def clicked(self, button, calendar_view_function):
		"""
		Callback function for the Forward, Backward, and Today buttons.
		"""
		calendar_view_function()


class CalendarPopup(Gtk.Menu):
	"""
	Popup menu used by Calendar to allow a user to add or remove a
	journal entry.
	"""
	def __init__(self, calendar, calendar_day):
		"""
		calendar: Calendar instance
		calendar_day: CalendarDay instance
		"""
		Gtk.Menu.__init__(self)
		self.calendar = calendar
		self.calendar_day = calendar_day
		self.calendar_view = calendar.calendar_view
		self.journal = calendar.journal
		year, month, day = calendar.get_ymd()

		self.add(Gtk.MenuItem(label=" ".join((calendar.month_name_label.get_text(), str(day)))))
		self.add(Gtk.SeparatorMenuItem())

		# see if an cycle journal entry already exists for this day
		journal_entry = self.journal.get_journal_entry(year, month, day, JournalEntryTypeKnown.CYCLE)
		if journal_entry is not None:
			is_active = True
		else:
			is_active = False
		menu_item = Gtk.CheckMenuItem.new_with_label(_("Menstrual Cycle Began"))
		menu_item.set_active(is_active)
		menu_item.connect("activate", self.activate, (JournalEntryTypeKnown.CYCLE, journal_entry))
		self.add(menu_item)

		# see if a tablet journal entry already exists for this day
		journal_entry = self.journal.get_journal_entry(year, month, day, JournalEntryTypeKnown.CONCEPTION)
		if journal_entry is not None:
			is_active = True
		else:
			is_active = False
		menu_item = Gtk.CheckMenuItem.new_with_label(_("Conception Occurred"))
		menu_item.set_active(is_active)
		menu_item.connect("activate", self.activate, (JournalEntryTypeKnown.CONCEPTION, journal_entry))
		self.add(menu_item)

		# see if a tablet journal entry already exists for this day
		journal_entry = self.journal.get_journal_entry(year, month, day, JournalEntryTypeKnown.TABLET)
		if journal_entry is not None:
			is_active = True
		else:
			is_active = False
		menu_item = Gtk.CheckMenuItem.new_with_label(_("Took 1st Tablet"))
		menu_item.set_active(is_active)
		menu_item.connect("activate", self.activate, (JournalEntryTypeKnown.TABLET, journal_entry))
		self.add(menu_item)

		# see if a note journal entry already exists for this day
		journal_entry = self.journal.get_journal_entry(year, month, day, JournalEntryTypeKnown.NOTE)
		if journal_entry is None:
			menu_item = Gtk.MenuItem.new_with_label(_("Add Note"))
			menu_item.connect("activate", self.activate, (JournalEntryTypeKnown.NOTE, None))
		else:
			menu_item = Gtk.MenuItem.new_with_label(_("Edit Note"))
			menu_item.connect("activate", self.activate, (JournalEntryTypeKnown.NOTE, journal_entry))
		self.add(menu_item)
		self.show_all()

	def activate(self, menu_item, data):
		"""
		Callback function for popup menu.
		"""
		journal_entry_type = data[0]
		journal_entry = data[1]
		if journal_entry_type == JournalEntryTypeKnown.NOTE:
			# get the main window
			parent = self.calendar.get_toplevel()
			year, month, day = self.calendar_day.get_ymd()
			note_window = NoteWindow(
				parent=parent,
				journal=self.journal,
				journal_entry=journal_entry,
				date=new_date(day, month, year))
			parent.get_application().add_window(note_window)
			note_window.show_all()
		else:
			if journal_entry is None:
				self.journal.new_entry(self.calendar_day, journal_entry_type)
			else:
				self.journal.remove_entry(journal_entry)


class CalendarView(Gtk.Box):
	"""
	CalendarView is the primary interface to view journal entries as
	well as predictions. It is meant to be embedded in MainWindow.
	"""
	def __init__(self, journal=None, settings=None, color_settings=None):
		Gtk.Box.__init__(self, homogeneous=True, spacing=12)
		self.journal = journal
		self.settings = settings
		self.color_settings = color_settings
		self.settings.connect("changed", self.settings_changed)
		self.color_settings.connect("changed", self.color_settings_changed)

		self.calendar_key = CalendarKey(self.color_settings)
		self.settings_changed(self.settings, "show-calendar-key")

		columns=self.settings.get_int("calendar-view-columns")
		rows=self.settings.get_int("calendar-view-rows")

		self.pack_start(self.calendar_key, True, True, 0)
		self.set_size(columns, rows)

		self.show()

		self.journal.connect("add-entry", self.refresh)
		self.journal.connect("remove-entry", self.refresh)
		self.journal.connect("stats-updated", self.refresh)

	def backward(self):
		"""
		Decrement all months by the number of calendars on the grid.
		"""
		for calendar in self.calendars:
			old_year, old_month, old_day = calendar.get_ymd()
			new_month = ((old_month - 1 - len(self.calendars)) % 12) + 1
			new_year = old_year + ((old_month - 1 - len(self.calendars)) // 12)
			calendar.set_month_year(new_month, new_year)

	def color_settings_changed(self, color_settings, name):
		"""
		Callback function for GSettings.
		"""
		color = color_settings.get_string(name)
		for calendar in self.calendars:
			calendar.set_color(name, color)

	def current(self):
		"""
		Set calendars so that first in the series is the current month.
		"""
		date = GLib.DateTime.new_now_local()
		current_month = date.get_month()
		current_year = date.get_year()

		for calendar_number in range(len(self.calendars)):
			calendar = self.calendars[calendar_number]
			month = ((current_month - 1 + calendar_number) % 12) + 1
			year = current_year + ((current_month - 1 + calendar_number) // 12)
			calendar.set_month_year(month, year)

	def forward(self):
		"""
		Increment all months by the number of calendars on the grid.
		"""
		for calendar in self.calendars:
			old_year, old_month, old_day = calendar.get_ymd()
			new_month = ((old_month - 1 + len(self.calendars)) % 12) + 1
			new_year = old_year + ((old_month - 1 + len(self.calendars)) // 12)
			calendar.set_month_year(new_month, new_year)

	def refresh(self, journal=None):
		"""
		Set the colors on each calendar.
		"""
		for calendar in self.calendars:
			calendar.refresh()

	def set_size(self, columns=None, rows=None):
		"""
		Set the number of columns and rows of calendars displayed on
		the CalendarView.

		columns: integer greater than or equal to 1.
		rows: integer greater than or equal to 1.
		"""
		if isinstance(columns, int) and columns >= 1:
			self.columns = columns
		if isinstance(rows, int) and rows >= 1:
			self.rows = rows

		self.calendars = []

		colors = {}
		for key in self.color_settings.keys():
			colors[key] = self.color_settings.get_string(key)

		method = self.settings.get_enum("fertility-awareness-method")
		birth_control_duration = self.settings.get_int("birth-control-duration")
		birth_control_inactive_days = self.settings.get_int("birth-control-inactive-days")
		for calendar_number in range(self.rows * self.columns):
			calendar = Calendar(
				calendar_view=self,
				journal=self.journal,
				method=method,
				birth_control_duration=birth_control_duration,
				birth_control_inactive_days=birth_control_inactive_days,
				colors=colors)
			calendar.show_all()
			self.calendars.append(calendar)
		self.current()

		for child in self.get_children():
			if child != self.calendar_key:
				child.destroy()

		for column in range(self.columns):
			vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
			self.pack_start(vbox, True, True, 0)
			vbox.show()
			for row in range(self.rows):
				vbox.pack_start(self.calendars[column + (row * self.columns)], True, True, 0)

		window = self.get_toplevel()
		if isinstance(window, Gtk.Window):
			window.resize(1, 1)

	def settings_changed(self, settings, key):
		"""
		Callback function for GSettings.
		"""
		if key == "calendar-view-columns":
			self.set_size(columns=settings.get_int(key))
		elif key == "calendar-view-rows":
			self.set_size(rows=settings.get_int(key))
		elif key == "show-calendar-key":
			show_key = settings.get_boolean(key)
			if not show_key and self.calendar_key.get_visible():
				key_width = self.calendar_key.get_allocated_width()
				window = self.get_toplevel()
				if isinstance(window, Gtk.ApplicationWindow):
					window_width = window.get_allocated_width()
					window_height = window.get_allocated_height()
					self.calendar_key.hide()
					window.resize(window_width - key_width, window_height)
					#window.resize(1, window_height)
			elif show_key:
				self.calendar_key.show()
		elif key == "fertility-awareness-method":
			method = settings.get_enum(key)
			for calendar in self.calendars:
				calendar.set_fertility_awareness_method(method)
		elif key == "birth-control-duration":
			days = settings.get_int(key)
			for calendar in self.calendars:
				calendar.set_birth_control_duration(days)
		elif key == "birth-control-inactive-days":
			days = settings.get_int(key)
			for calendar in self.calendars:
				calendar.set_birth_control_inactive_days(days)


class EizelleError(Exception):
	"""
	Custom exception for handling non-fatal errors in this module.

	EizelleError.long_message: String explaining what the problem is.
	This should be no more than a few sentences that would be suitable
	to display in a dialog window.

	EizelleError.short_message: String summarizing the problem. This
	should be terse and no more than a few words that would be suitable
	to display on a window titlebar or header.
	"""
	def __init__(
			self,
			long_message=None,
			short_message=None,
			*args):
		"""
		long_message: string explaing the problem in a few
		              sentences or less
		short_message: string with a terse summary no more than a few
		               words that would be suitable for a window
		               titlebar or header.
		args: not used. present for compatibility with other exceptions
		"""
		if isinstance(long_message, str):
			self.long_message = long_message
		else:
			self.long_message = _("An unknown error has occurred")

		if isinstance(short_message, str):
			self.short_message = short_message
		else:
			self.short_message = _("Eizelle Error")

class ErrorDialog(Gtk.Dialog):
	"""
	Dialog window for displaying error messages to the user.
	"""
	def __init__(self, parent, error_message, title=_("Error")):
		"""
		error_message: string containing message to display
		parent: GtkWindow instance
		title: string containing terse description for the window title
		"""
		Gtk.Dialog.__init__(
			self,
			title=title,
			border_width=12,
			transient_for=parent)

		label = Gtk.Label.new(error_message)
		vbox = self.get_content_area()
		vbox.set_spacing(12)
		vbox.add(label)

		self.add_buttons(_("Quit"), Gtk.ResponseType.OK)
		self.set_default_response(Gtk.ResponseType.OK)


class FertilityAwarenessMethod:
	STANDARD_DAYS=0
	RHYTHM=1


class Journal(GObject.GObject):
	"""
	Journal is used to add, to view, and to remove journal entries as
	well as to access satistics about those entries. If journal data
	is read from a file via Journal.load_from_file(), any changes to the
	journal data will be saved automatically. Journal can also be used
	to encrypt journal data saved to a file by setting a password
	via Journal.set_password().
	"""
	__gsignals__ = { 'add-entry': (GObject.SIGNAL_RUN_FIRST, None, tuple()),
				 'stats-updated': (GObject.SIGNAL_RUN_FIRST, None, tuple()),
				  'remove-entry': (GObject.SIGNAL_RUN_FIRST, None, tuple())}

	def __init__(self, cycle_lower_limit=26, cycle_upper_limit=32):
		"""
		cycle_lower_limit: integer representing the shortest length (in
		days) of menstrual cycle that should be included in the
		statistics and used for predictions. This can also be changed
		via Journal.set_cycle_lower_limit().

		cycle_upper_limit: integer representing the longest length (in
		days) of a menstrual cycle that should be included in the
		statistics and used for predictions. This can also be changed
		via Journal.set_cycle_upper_limit().
		"""
		GObject.GObject.__init__(self)
		self._store_cycle_lower_limit(cycle_lower_limit)
		self._store_cycle_upper_limit(cycle_upper_limit)

		# data serialization format for this version of eizelle
		self._file_format_version = 0

		self._clear()

	def _clear(self):
		"""
		Reset journal instance to an empty state. This does not affect
		any data stored on disk. It should not be used directly since
		it is called automatically before loading journal data via
		Journal.load_from_file().

		Returns: None
		"""
		self._working_file = None
		self._encryption = None
		self._password = None
		self._props = {"version" : 0}

		self.entries = {}
		self.first_entry = {}
		self.last_entry = {}

		for type_id in JournalEntryTypeKnown:
			self.entries[type_id] = {}
			self.first_entry[type_id] = None
			self.last_entry[type_id] = None

		self._update_statistics()

	def _decrypt(self):
		"""
		Decrypt journal data that has already been read from a file
		via Journal.load_from_file. It should not be used directly since
		it is called automatically from Journal.set_password().

		Returns: None
		"""
		if self.is_locked() and	self._password is not None and self.has_crypto_support():
			if self._encryption["version"] == 0:
				salt = b64decode(self._encryption["salt"].encode(encoding="utf-8"))
				derivation_iterations = self._encryption["derivation_iterations"]
				derived_key = self._password.encode(encoding="utf-8")
				for i in range(derivation_iterations):
					derived_key = SHA256.new(derived_key + salt).digest()
				block_size = AES.block_size
				iv = b64decode(self._encryption["iv"].encode(encoding="utf-8"))
				encoded_cipher_text = self._file_data
				cipher_text = b64decode(encoded_cipher_text.encode(encoding="utf-8"))
				cipher = AES.new(derived_key, AES.MODE_CBC, iv)
				padded_clear_text = cipher.decrypt(cipher_text)
				padding_length = padded_clear_text[-1]
				# Python 2.x compatibility
				if not isinstance(padding_length, int):
					padding_length = ord(padding_length)
				clear_text = padded_clear_text[:-padding_length]
				hash_ = SHA256.new(clear_text).hexdigest()
				if hash_ == self._encryption["hash"]:
					self._file_data = json.loads(clear_text.decode(encoding="utf-8"))
					self._encryption = None
			else:
				raise EizelleError("Journal._decrypt encountered an unknown encryption type.")
			self._parse_file_data()

	def _encrypt(self, journal_data, version=MAX_SUPPORTED_ENCRYPTION_VERSION):
		"""
		Encrypt journal data when writing it to a file. It should not be
		used directly since it is called automatically from
		Journal.save_to_file().

		journal_data: dictionary containing journal entries and journal
		              properties

		version: integer representing the encryption version to use

		Returns: A tuple containing a dictionary with the encryption
		metadata and the encrypted data itself.
		"""
		if (self._password is not None and
			self.has_crypto_support() is True):
			if version == 0:
				salt_length = 32
				salt = urandom(salt_length)
				derivation_iterations = 100
				derived_key = self._password.encode(encoding="utf-8")
				for i in range(derivation_iterations):
					derived_key = SHA256.new(derived_key + salt).digest()
				block_size = AES.block_size
				iv = urandom(block_size)
				clear_text = json.dumps(journal_data).encode(encoding="utf-8")
				hash_ = SHA256.new(clear_text).hexdigest()
				padding_length = block_size - (len(clear_text) % block_size)
				padding = (chr(padding_length) * padding_length)
				padded_clear_text = clear_text + padding.encode(encoding="utf-8")
				cipher = AES.new(derived_key, AES.MODE_CBC, iv)
				cipher_text = cipher.encrypt(padded_clear_text)
				encoded_cipher_text = b64encode(cipher_text)
				return {"version": 0, "hash": hash_, "iv": b64encode(iv).decode(encoding="utf-8"), "salt": b64encode(salt).decode(encoding="utf-8"), "derivation_iterations": derivation_iterations}, encoded_cipher_text.decode(encoding="utf-8")
			else:
				raise EizelleError("Journal._encrypt called with invalid version")
		else:
			return None, journal_data

	def _generate_default_filenames(self):
		"""
		Returns a list of the default filenames used by the journal
		in order from newest to oldest. It should not be called directly
		since it is called automatically when Journal.load_from_file()
		is called without a filename.

		Returns: list of strings
		"""
		filename_list = []

		# GLib.get_user_data_dir reads XDG_DATA_HOME if available,
		# otherwise it will use ~/.local/share as a fallback
		base_directory = GLib.get_user_data_dir()
		if base_directory is None:
			raise EizelleError("Neither XDG_DATA_HOME nor HOME environmental variables have been set.")

		program_data_directory = GLib.build_filenamev([base_directory, GLib.get_prgname().lower()])

		# journal filename used since Eizelle version 0.6
		filename_list.append(GLib.build_filenamev([program_data_directory, "journal.json"]))

		# journal filename used for Eizelle versions 0.4 and 0.5
		filename_list.append(GLib.build_filenamev([program_data_directory, "journal"]))

		return filename_list


	def _generate_key(self, date):
		"""
		Generates a key used to access journal entries. They key is
		currently a string representing a date in the form "2000-01-01".
		There is no need to call this directly. It is called
		automatically via JournalEntry.get_key().

		Returns: a string
		"""
		if isinstance(date, GLib.Date):
			year = date.get_year()
			month = int(date.get_month())
			day = date.get_day()
		else:
			raise EizelleError("Journal._generate_key called with invalid date.")

		key = "-".join((str(year), str(month).rjust(2, '0'), str(day).rjust(2, '0')))
		return key

	def _insert_entry(self, journal_entry, save_state=True, update_statistics=True):
		"""
		Insert a JournalEntry into to the Journal.

		By default, the Journal state will be saved upon each time a new
		JournalEntry is inserted but save_state allows this to be
		overridden when the entire data set is being loaded.

		By default, the Journal statistics will be updated each time a
		new JournalEntry is inserted but update_statistics allows this
		to be overridden when the entire data set is being loaded.

		It is for internal use and should not be called directly. If you
		wish to enter a new journal entry, you can do so via
		Journal.new_entry().

		Returns: None
		"""
		if isinstance(journal_entry, JournalEntry):
			type_id = journal_entry.get_type_id()
			key = journal_entry.get_key()

			if type_id not in self.entries:
				self.entries[type_id] = {}
			self.entries[type_id][key] = journal_entry

			if update_statistics:
				self._update_statistics()

			if save_state:
				self._save_to_file()

			self.emit("add-entry")

	def _parse_file_data(self):
		"""
		Parses journal data that has been loaded from a file via
		Journal.load_from_file(). There is no need to call this directly.

		Returns: None
		"""
		if self.is_locked():
			return False

		file_data = self._file_data
		journal_entries = file_data["journal_entries"]
		self._props = file_data

		# restore journal entries
		for journal_entry_props in journal_entries:

			# convert to new date format
			if "year" in journal_entry_props and "month" in journal_entry_props and "day" in journal_entry_props:
				journal_entry_props["date"] = {"year" : journal_entry_props["year"], "month" : journal_entry_props["month"], "day" : journal_entry_props["day"]}
				del journal_entry_props["year"]
				del journal_entry_props["month"]
				del journal_entry_props["day"]

			# convert to new JournalEntryType nomenclature
			if "type_id" not in journal_entry_props:
				if "category" in journal_entry_props:
					journal_entry_props["type_id"] = journal_entry_props["category"]
					del journal_entry_props["category"]

			self._insert_entry(JournalEntry(self, journal_entry_props), save_state=False, update_statistics=False)

		self._update_statistics()

	def _save_to_file(self):
		"""
		Store all journal entries and journal metadata to a file.

		Journal entries and journal metadata are stored to a single file
		in JSON format. The data stored is a dictionary with the
		following keys:
		  * version: integer representing version of the file format
		  * encryption: None or dicionary containing encryption metadata
		  * journal_data: dictionary (containing journal entries and
		                  journal metadata) or the encrypted data itself

		At minimum, each journal entry dictionary contains the following
		keys:
		 * date: a dictionary containing "year", "month", and "day"
		         keys, each one represented by a integer
		 * type_id: string identifier for a member of
		            JournalEntryTypeKnown


		Returns: True if data was written, False if the operation was
		aborted due to not meeting both of the following requirements:
		 * a file must have been loaded successfully via
		   Journal.load_from_file(). The file must exist and contain
		   valid data unless the default filename was used.
		 * if the file loaded via Journal.load_from_file() is encrypted,
		   it will not be re-saved until a valid password has been
		   provided via Journal.set_password().
		"""
		# Do not proceed if journal data was loaded from a file but is
		# still encrypted. This ensures that encrypted data does not get
		# overwritten in the event a correct password was not supplied.
		if self.is_locked():
			return False


		# do not proceed if data was not read from a file
		if self._working_file is None:
			return False
		else:
			journal_filename = self._working_file

		program_data_directory = GLib.path_get_dirname(journal_filename)
		if not GLib.file_test(program_data_directory, GLib.FileTest.EXISTS):
			# Create with 700 permissions per XDG base directory
			# specification. Permissions value is in base 10 for
			# compatibility with Python 2 and Python 3.
			GLib.mkdir_with_parents(program_data_directory, 448)

		# generate list of properties needed to restore the journal entries
		journal_entries = []
		for type_id in self.entries:
			for key in self.entries[type_id]:
				journal_entries.append(self.entries[type_id][key].get_properties())

		journal_data = self._props
		journal_data["journal_entries"] = journal_entries

		# encrypt data if password was supplied
		encryption, journal_data = self._encrypt(journal_data)

		file_data = {"version" : self._file_format_version,
				  "encryption" : encryption,
			    "journal_data" : journal_data}

		with open(journal_filename, "w") as file_:
			json.dump(file_data, file_)
			return True

	def _store_cycle_lower_limit(self, cycle_lower_limit):
		"""
		Validate and save a new value for cycle_lower_limit. This method
		is strictly for internal use and should not be called directly.

		If you wish to change the value of cycle_lower_limit, do so via
		Journal.set_cycle_lower_limit().

		Returns: None
		"""
		if not isinstance(cycle_lower_limit, int):
			raise EizelleError

		if cycle_lower_limit >= 0:
			self._cycle_lower_limit = cycle_lower_limit
		else:
			raise EizelleError

	def _store_cycle_upper_limit(self, cycle_upper_limit):
		"""
		Validate and save a new value for cycle_upper_limit. This method
		is strictly for internal use and should not be called directly.

		If you wish to change the value of cycle_upper_limit, do so via
		Journal.set_cycle_upper_limit().

		Returns: None
		"""
		if not isinstance(cycle_upper_limit, int):
			raise EizelleError

		if cycle_upper_limit >= 0:
			self._cycle_upper_limit = cycle_upper_limit
		else:
			raise EizelleError

	def _update_statistics(self):
		"""
		Re-calculate the statistics for various types of journal
		entries. This is for internal use and will be called
		automatically when necessary.

		Returns: None
		"""
		self.average_cycle_length = None
		self.shortest_cycle_length = None
		self.shortest_cycle_entry = None
		self.longest_cycle_length = None
		self.longest_cycle_entry = None

		# reset the statistics for known journal entry types.
		self.first_entry = {}
		self.last_entry = {}
		for type_id in JournalEntryTypeKnown:
			self.first_entry[type_id] = None
			self.last_entry[type_id] = None

		# determine entries with earliest date and latest date
		# for both known and unrecognized journal entry types.
		for type_id in self.entries:

			self.first_entry[type_id] = None
			self.last_entry[type_id] = None

			if len(self.entries[type_id]) > 0:
				sorted_keys = sorted(self.entries[type_id])
				first_key = sorted_keys[0]
				last_key = sorted_keys[-1]
				self.first_entry[type_id] = self.entries[type_id][first_key]
				self.last_entry[type_id] = self.entries[type_id][last_key]

		### menstrual cycle statistics ###
		type_id = JournalEntryTypeKnown.CYCLE

		if len(self.entries[type_id]) > 0:

			sorted_keys = sorted(self.entries[type_id].keys())

			# calculate average menstrual cycle length while excluding
			# menstrual cycles that exceed the length of the upper limit
			# or fall below the lower limit (possibly due to pregnancy,
			# use of birth control, user error, etc.)
			if len(sorted_keys) >= 2:
				cycle_lengths = []
				for key_number in range(len(sorted_keys) - 1):
					entry1 = self.entries[type_id][sorted_keys[key_number]]
					entry2 = self.entries[type_id][sorted_keys[key_number + 1]]
					difference = entry1.get_date().days_between(entry2.get_date())
					if (difference <= self._cycle_upper_limit and
							difference >= self._cycle_lower_limit):
						cycle_lengths.append(difference)

						# determine shortest menstrual cycle
						if (self.shortest_cycle_length is None or
								difference <= self.shortest_cycle_length):
							self.shortest_cycle_length = difference
							self.shortest_cycle_entry = entry2

						# determine longest menstrual cycle
						if (self.longest_cycle_length is None or
								difference >= self.longest_cycle_length):
							self.longest_cycle_length = difference
							self.longest_cycle_entry = entry2

				# determine average menstrual cycle length
				if len(cycle_lengths) > 0:
					self.average_cycle_length = int(round(float(sum(cycle_lengths)) / len(cycle_lengths)))
				else:
					self.average_cycle_length = None

		self.emit("stats-updated")

	def get_all_entries(self, journal_entry_type=None):
		"""
		Returns a list of journal entries of type journal_entry_type. If
		journal_entry_type is None, return a list of all journal
		journal entries.

		Returns: a list of 0 or more JournalEntry objects
		"""
		if (journal_entry_type is not None and
				not journal_entry_type in JournalEntryTypeKnown and
				not journal_entry_type in JournalEntryTypeUnknown):
			raise EizelleError("Journal.get_all_entries called with invalid journal_entry_type.")

		entries = []
		if journal_entry_type is None:
			for type_id in self.entries:
				for entry in self.entries[type_id]:
					entries.append(self.entries[type_id][entry])
		elif journal_entry_type == JournalEntryTypeUnknown.ALL:
			for type_id in self.entries:
				if type_id not in JournalEntryTypeKnown:
					for entry in self.entries[type_id]:
						entries.append(self.entries[type_id][entry])
		else:
			type_id = journal_entry_type
			for entry in self.entries[type_id]:
				entries.append(self.entries[type_id][entry])
		return entries

	def get_average_cycle_length(self):
		"""
		Returns the length in days of the average menstrual cycle based
		on the existing menstrual cycle journal entries.

		Returns: int
		"""
		return self.average_cycle_length

	def get_first_entry(self, journal_entry_type):
		"""
		Returns the journal entry with the earliest date of type
		journal_entry_type or None if no journal entries exist for that
		type.

		Returns: JournalEntry or None
		"""
		if not journal_entry_type in JournalEntryTypeKnown:
			raise EizelleError("Journal.get_first_entry called with invalid journal_entry_type.")

		type_id = journal_entry_type
		return self.first_entry[type_id]

	def get_journal_entry(self, year, month, day, journal_entry_type):
		"""
		Returns an existing journal entry of type journal_entry_type
		for the given date if it exists. Otherwise returns None.

		Returns: JournalEntry or None
		"""
		if not journal_entry_type in JournalEntryTypeKnown:
			raise EizelleError("Journal.get_journal_entry called with invalid journal_entry_type.")

		key = self._generate_key(new_date(day, month, year))
		type_id = journal_entry_type
		if key in self.entries[type_id]:
			journal_entry = self.entries[type_id][key]
			return journal_entry
		else:
			return None

	def get_last_entry(self, journal_entry_type):
		"""
		Returns the journal entry with the latest date of type
		journal_entry_type or None if no journal entries exist for that
		type.

		Returns: JournalEntry or None
		"""
		if not journal_entry_type in JournalEntryTypeKnown:
			raise EizelleError("Journal.get_last_entry called with invalid journal_entry_type.")

		type_id = journal_entry_type
		return self.last_entry[type_id]

	def get_longest_cycle_entry(self):
		"""
		Returns the menstrual cycle entry with the longest duration. If
		more than one menstural cycle has an equally long duration, only
		the one with the latest date will be returned.

		Returns: JournalEntry or None
		"""
		return self.longest_cycle_entry

	def get_longest_cycle_length(self):
		"""
		Returns the length in days of the longest recorded menstrual
		cycle.

		Returns: int or None
		"""
		return self.longest_cycle_length

	def get_shortest_cycle_entry(self):
		"""
		Returns the menstrual cycle entry with the shortest duration. If
		more than one menstrual cycle has an equally short duration,
		only the one with the latest date will be returned.

		Returns: JournalEntry or None
		"""
		return self.shortest_cycle_entry

	def get_shortest_cycle_length(self):
		"""
		Returns the length in days of the shortest recorded menstrual
		cycle.

		Returns: int or None
		"""
		return self.shortest_cycle_length

	def has_crypto_support(self):
		"""
		Indicates whether or not the libraries required for encryption
		are avaialable.

		Returns: True if journal files can be encrypted and decrypted,
		False if encryption and decryption support are unavailable.

		Returns: bool
		"""
		if AES is not None and SHA256 is not None:
			return True
		else:
			return False

	def get_total_entries(self, journal_entry_type=None):
		"""
		Returns total number of JournalEntry objects in Journal for
		given JournalEntryTypeKnown or JournalEntryTypeUnknown.

		If journal_entry_type is omitted, return the total number of all
		JournalEntry objects in the Journal.

		Returns: int
		"""
		if journal_entry_type is None:
			entry_totals = []
			for type_id in self.entries:
				entry_totals.append(len(self.entries[type_id]))
			total = sum(entry_totals)
		elif journal_entry_type in JournalEntryTypeKnown:
			type_id = journal_entry_type
			total = len(self.entries[type_id])
		elif journal_entry_type in JournalEntryTypeUnknown:
			entry_totals = []
			for type_id in self.entries:
				if type_id not in JournalEntryTypeKnown:
					entry_totals.append(len(self.entries[type_id]))
			total = sum(entry_totals)
		else:
			raise EizelleError("Journal.get_total_entries called with invalid journal_entry_type.")

		return total

	def is_encrypted(self):
		"""
		Returns True if the journal data loaded from a file via
		Journal.load_from_file() has not been decrypted or if
		Journal.set_password() has been given a value other than None.

		If True, Journal._save_to_file() will store Journal data in the
		default encrypted format and a password will be required to open
		it.

		If False, Journal._save_to_file() will store Journal data
		in clear text JSON format and no password will be required to
		open it.

		Returns: bool
		"""
		if self.is_locked() or (self._password is not None):
			return True
		else:
			return False

	def is_locked(self):
		"""
		Returns True if an Journal.load_from_file() has opened an
		encrypted file but Journal.set_password() has not been given a
		valid password to open it. Otherwise, returns False.

		Returns: bool
		"""
		if self._encryption is None:
			return False
		else:
			return True

	def load_from_file(self, filename=None, password=None):
		"""
		Load existing journal data from the given file.

		filename: string or None
		password: string or None

		If a filename is given, it must contain at least one character.

		If no filename is given, Eizelle will try to open the default
		filename. If this fails, Eizelle will try to open the default
		filenames used in previous versions of the program. If none of
		these files exist, Eizelle will continue without error.

		If a filename is given, Eizelle will try to open it and raise an
		exception if this fails.

		If a password is provided and the journal data is encrypted,
		decryption will be attempted by using the given password. If a
		password is provided but the journal data is not encrypted, it
		will be immediately encrypted and saved to the original file.

		Returns: None
		"""
		if filename is not None:
			if not isinstance(filename, str):
				raise EizelleError(_("Journal.load_from_file was passed in invalid argument"))
			if len(filename) == 0:
				raise EizelleError(_("Filename must contain at least one character"))

		self._clear()

		if filename is not None:
			filename_list = [filename]
		else:
			filename_list = self._generate_default_filenames()

		file_data = None
		for journal_filename in filename_list:
			if GLib.file_test(journal_filename, GLib.FileTest.EXISTS):
				with open(journal_filename, "rb") as file_:
					try:
						file_data = pickle.load(file_)
					except Exception:
						with open(journal_filename, "r") as file_:
							try:
								file_data = json.load(file_)
							except Exception:
								raise EizelleError(_("Unable to load journal data."))
				if file_data is not None:
					break

			# if filename does not exist and filename is not the default
			else:
				if filename is not None:
					raise EizelleError(_("Unable to load journal data."))

		self._working_file = filename_list[0]

		# Do not continue parsing if there is no data to restore (e.g.
		# the first time the program is run).
		if file_data is None:
			return None

		# file format used for Eizelle versions 0.4 and 0.5
		if isinstance(file_data, list) and len(file_data) >= 1 and isinstance(file_data[0], dict) and "version" in file_data[0] and file_data[0]["version"] == 0:
			#self._props = file_data[0]
			self._encryption = None
			self._file_data = {"version" : 0, "journal_entries" : file_data[1:]}

		# file format used since Eizelle version 0.6
		elif isinstance(file_data, dict) and "version" in file_data and file_data["version"] <= self._file_format_version:
			if file_data["encryption"] is None:
				self._encryption = file_data["encryption"]
				self._file_data = file_data["journal_data"]
			elif file_data["encryption"]["version"] > MAX_SUPPORTED_ENCRYPTION_VERSION:
				raise EizelleError(_("Journal data was encrypted with a newer version of this program and cannot be read."))
			else:
				self._encryption = file_data["encryption"]
				self._file_data = file_data["journal_data"]
		else:
			raise EizelleError(_("Journal data was saved with a newer version of this program and cannot be read."))

		if password is not None:
			self.set_password(password)

		self._parse_file_data()

	def new_entry(self, date_object, journal_entry_type, note=None):
		"""
		Enters a journal entry into the journal.

		date_object can be one of three object types:
		 * a tuple containing day, month, year where day, month, and
		   year are each an integer. E.g. (1, 1, 2000)
		 * an instance of GLib.Date
		 * an instance of CalendarDay

		 note: a string

		Returns: None
		"""
		if not journal_entry_type in JournalEntryTypeKnown:
			raise EizelleError("Journal.new_entry called with invalid journal_entry_type.")

		if isinstance(date_object, CalendarDay):
			year, month, day = date_object.get_ymd()
			date = new_date(day, month, year)

		elif isinstance(date_object, tuple):
			year, month, day = date_object
			date = new_date(day, month, year)

		elif isinstance (date_object, GLib.Date):
			date = date_object
			year = date_object.get_year()
			month = int(date_object.get_month())
			day = date_object.get_day()

		else:
			raise EizelleError("Journal.new_entry called with invalid date_object.")

		if (note is not None and
			not isinstance(note, str)):
				raise EizelleError("Journal.new_entry called with invalid note.")

		if (journal_entry_type == JournalEntryTypeKnown.NOTE and
			note is None):
			raise EizelleError("Journal.new_entry called with invalid note.")

		props = {}
		props["date"] = {"year" : year, "month" : month, "day" : day}
		props["type_id"] = str(journal_entry_type)
		if note is not None:
			props["note"] = note

		journal_entry = JournalEntry(self, props)

		self._insert_entry(journal_entry)

	def remove_entry(self, journal_entry):
		"""
		Removes a journal entry from the journal.

		journal_entry: JournalEntry object

		Returns: None
		"""
		if not isinstance(journal_entry, JournalEntry):
			raise EizelleError
		type_id = journal_entry.get_type_id()
		key = journal_entry.get_key()
		del self.entries[type_id][key]

		self._update_statistics()

		self._save_to_file()
		self.emit("remove-entry")

	def set_cycle_lower_limit(self, cycle_lower_limit):
		"""
		cycle_lower_limit: integer representing the shortest length (in
		days) of menstrual cycle that should be included in the
		statistics and used for predictions.

		Returns: None
		"""
		self._store_cycle_lower_limit(cycle_lower_limit)
		self._update_statistics()

	def set_cycle_upper_limit(self, cycle_upper_limit):
		"""
		cycle_upper_limit: integer representing the longest length (in
		days) of a menstrual cycle that should be included in the
		statistics and used for predictions.

		Returns: None
		"""
		self._store_cycle_upper_limit(cycle_upper_limit)
		self._update_statistics()

	def set_password(self, password=None):
		"""
		password: a string from which an encryption key will be derived

		This method should be called to decrypt journal data that has
		already been read from a file via Journal.load_from_file(). If
		the decryption suceeds (or if the data was not already
		encrypted) the password will be used to encrypt it.

		If the password is set to None, the journal data will be saved
		to file without encryption.

		Returns: None
		"""
		if (not isinstance(password, str) and
			password is not None):
			raise EizelleError("Journal.set_password was passed an invalid argument")

		if not self.has_crypto_support():
			raise EizelleError("Missing required dependencies for encryption.")

		# if the password has changed, store the new one
		if self._password != password:
			self._password = password

			# if a password exists and the journal is locked, attempt
			# to unlock (decrypt) it.
			if password is not None and self.is_locked():
				self._decrypt()
			# Save the journal if the password has been changed and
			# the journal is not locked.
			else:
				self._save_to_file()


class JournalEntry:
	"""
	Each record entered by the user is stored in the journal as a
	JournalEntry.

	A new journal entry should be created via Journal.new_entry().
	"""
	def __init__(self, journal, properties):
		"""
		journal: the Journal in which this journal entry will be stored
		properties: a dictionary of journal entry metadata
		"""
		# validate journal
		if not isinstance(journal, Journal):
			raise EizelleError("JournalEntry initialized with invalid journal.")

		# validate properties
		if not isinstance(properties, dict):
			raise EizelleError("JournalEntry initialized with invalid properties.")

		# check for all required JournalEntry properties
		required_properties = ("date", "type_id")
		for property_name in required_properties:
			if not property_name in properties:
				raise EizelleError(" ".join(("JournalEntry initialized without required property:", property_name)))

		# validate date
		if not isinstance(properties["date"], dict):
			raise EizelleError("JournalEntry initalized with invalid date.")
		for date_member in ("day", "month", "year"):
			if (not date_member in properties["date"] or
				not isinstance(properties["date"][date_member], int)):
				raise EizelleError("JournalEntry initalized with invalid date.")

		if not isinstance(properties["type_id"], str):
			raise EizelleError("JournalEntry initialized with invalid type id.")

		self.journal = journal
		type_id = properties["type_id"]

		self._props = properties
		if type_id in JournalEntryTypeKnown:
			self._type = JournalEntryTypeKnown
		else:
			self._type = JournalEntryTypeUnknown
		self._type_id = type_id

		day = self._props["date"]["day"]
		month = self._props["date"]["month"]
		year = self._props["date"]["year"]
		self._date = new_date(
			self._props["date"]["day"],
			self._props["date"]["month"],
			self._props["date"]["year"])

	def get_date(self):
		"""
		Get the date that a journal entry marks. This is not necessarily
		the date that the entry was created.

		Returns: instance of GLib.Date
		"""
		return self._date

	def get_key(self):
		"""
		Returns a string used by Journal to look up this journal entry.

		They key is currently a string representing a date in the form
		"2000-01-01" but the implementation may change so you should not
		rely on any specific content for the key. You should use
		JournalEntry.get_date() to access the date for a journal entry.

		Returns: string
		"""
		return self.journal._generate_key(self._date)

	def get_note(self):
		"""
		Returns string for user-provided message attached to this
		JournalEntry or None if JournalEntry lacks a note.

		Returns: string or None
		"""
		if "note" in self._props:
			return self._props["note"]
		else:
			return None

	def get_properties(self):
		"""
		Returns a dictionary of properties containing the metadata
		required to recreate this journal entry. This is used by the
		journal for for data serialization.

		Returns: dict
		"""
		return self._props

	def get_type(self):
		return self._type

	def get_type_id(self):
		"""
		Returns string identifier for a member of JournalEntryTypeKnown.

		Note that if the journal data was loaded from a file that was
		created with a later version of Eizelle, this identifier may not
		exist in the JournalEntryTypeKnown defined in this version.

		Returns: string
		"""
		return self._type_id

	def get_ymd(self):
		"""
		Returns a date for a journal entry in (year, month, day) format.

		Returns: tuple
		"""
		return (
			self._props["date"]["year"],
			self._props["date"]["month"],
			self._props["date"]["day"])

	def has_note(self):
		"""
		Returns: True if the journal entry has a message attached,
		otherwise returns False.

		Returns: bool
		"""
		if "note" in self._props:
			return True
		else:
			return False


class JournalEntryType(set):
	def __getattr__(self, name):
		if name in self:
			return name
		raise EizelleError


JournalEntryTypeKnown = JournalEntryType([
	"NOTE",       # an arbitrary message entered by the user
	"CYCLE",      # first day of a new menstrual cycle
	"TABLET",     # first day of birth control prescription
	"CONCEPTION"]) # day that conception occurred


JournalEntryTypeUnknown = JournalEntryType([
	"ALL"]) # superset of all unrecognized journal entries


class MainWindow(Gtk.ApplicationWindow):
	"""
	This is the primary application window. It is used to view, enter,
	and edit data stored in the journal.

	It provides a normal mode for GTK+ 3.x versions >= 3.10 and a
	fallback mode for GTK+ 3.x versions that are less than 3.10.
	"""
	def __init__(self, journal, settings, color_settings):
		"""
		journal: Journal instance
		settings: GSettings instance
		color_settings: GSettings instance
		"""
		Gtk.ApplicationWindow.__init__(
			self,
			border_width=12,
			window_position=Gtk.WindowPosition.CENTER,
			title=PROGRAM_NAME)
		self.journal = journal
		self.settings = settings
		self.color_settings = color_settings

		accel_group = Gtk.AccelGroup()
		self.add_accel_group(accel_group)

		self.calendar_view = CalendarView(
			journal=self.journal,
			settings=self.settings,
			color_settings=self.color_settings)
		self.notes_view = NotesView(self.journal)
		self.statistics_view = StatisticsView(self.journal)

		# Gtk.Stack requires GTK+ 3.10
		try:
			stack = Gtk.Stack()
		except AttributeError:
			stack = None

		if stack is not None:
			stack.add_titled(self.calendar_view, "calendar", _("Calendar"))
			#stack.add_titled(self.notes_view, "notes", _("Notes"))
			stack.add_titled(self.statistics_view, "statistics", _("Statistics"))

			switcher = Gtk.StackSwitcher()
			switcher.set_stack(stack)

			self.calendar_navigation = CalendarNavigation(self.calendar_view)
			self.new_note_button = Gtk.Button.new_with_label(_("New"))
			self.new_note_button.connect("clicked", self.clicked, self.notes_view.new_note)
			stack.connect("notify::visible-child", self.stack_callback)

			headerbar = Gtk.HeaderBar(show_close_button=True, title=PROGRAM_NAME)
			headerbar.pack_start(self.calendar_navigation)
			headerbar.pack_start(self.new_note_button)
			headerbar.set_custom_title(switcher)
			self.set_titlebar(headerbar)
			self.add(stack)
			stack.show()

			menu = Gtk.Menu()
			key, modifier = Gtk.accelerator_parse("F9")
			menu_item = Gtk.CheckMenuItem.new_with_label(_("Show Calendar Key"))
			menu_item.set_active(self.settings.get_boolean('show-calendar-key'))
			self.settings.connect("changed::show-calendar-key", self.settings_changed, menu_item)
			menu_item.connect("activate", self.show_calendar_key_menu_callback)
			menu_item.add_accelerator("activate", accel_group, key, modifier, Gtk.AccelFlags.VISIBLE)
			menu.add(menu_item)

			preferences_menu_item = Gtk.MenuItem.new_with_label(_("Preferences"))
			preferences_menu_item.connect("activate", self.preferences_menu_callback, self)
			menu.add(preferences_menu_item)

			help_menu_item = Gtk.MenuItem.new_with_label(_("Help"))
			help_menu_item.connect("activate", self.help_menu_callback)
			menu.add(help_menu_item)

			about_menu_item = Gtk.MenuItem.new_with_label(_("About"))
			about_menu_item.connect("activate", self.about_menu_callback, self)
			menu.add(about_menu_item)

			menu.show_all()
			menu_button = Gtk.MenuButton(popup=menu)
			headerbar.pack_end(menu_button)
			headerbar.show_all()
			self.new_note_button.set_visible(False)

		# fallback mode for GTK+ versions < 3.10
		else:
			vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
			vbox.show()
			toolbar = Gtk.Toolbar()
			about_tool_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_ABOUT)
			about_tool_button.connect("clicked", self.about_button_callback, self)
			toolbar.insert(about_tool_button, 0)

			help_tool_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_HELP)
			help_tool_button.connect("clicked", self.help_button_callback, self)
			toolbar.insert(help_tool_button, 0)

			pref_tool_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_PREFERENCES)
			pref_tool_button.connect("clicked", self.preferences_button_callback, self)
			toolbar.insert(pref_tool_button, 0)

			separator_tool_item = Gtk.SeparatorToolItem()
			toolbar.insert(separator_tool_item, 0)

			forward_tool_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_FORWARD)
			forward_tool_button.connect("clicked", self.calendar_navigation_callback, "forward")
			toolbar.insert(forward_tool_button, 0)

			back_tool_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_BACK)
			back_tool_button.connect("clicked", self.calendar_navigation_callback, "backward")
			toolbar.insert(back_tool_button, 0)

			current_tool_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_HOME)
			current_tool_button.connect("clicked", self.calendar_navigation_callback, "current")
			toolbar.insert(current_tool_button, 0)

			toolbar.show_all()
			vbox.pack_start(toolbar, False, False, 0)
			vbox.pack_start(self.calendar_view, True, True, 0)
			self.add(vbox)

	def about_button_callback(self, tool_button, window):
		"""
		Callback function for the About button on the toolbar in
		fallback mode. This is not used in normal mode.
		"""
		about_window = AboutWindow(parent=window)
		self.get_application().add_window(about_window)
		about_window.run()
		about_window.destroy()

	def about_menu_callback(self, menu_item, window):
		"""
		Callback function for the About menu item. This is not used in
		fallback mode.
		"""
		about_window = AboutWindow(parent=window)
		self.get_application().add_window(about_window)
		about_window.run()
		about_window.destroy()

	def calendar_navigation_callback(self, widget, direction):
		"""
		Callback function for the calendar navigation widget on the
		headerbar in normal mode. This is not used in fallback mode.
		"""
		if direction == "backward":
			self.calendar_view.backward()
		elif direction == "forward":
			self.calendar_view.forward()
		elif direction == "current":
			self.calendar_view.current()

	def clicked(self, button, function):
		"""
		Callback function for the new note button on the notes view.
		"""
		function()

	def help_button_callback(self, tool_button, window):
		"""
		Callback function for the Help button on the toolbar in fallback
		mode. This is not used in normal mode.
		"""
		Gtk.show_uri(None, "".join(("help:", PROGRAM_NAME.lower())), Gdk.CURRENT_TIME)

	def help_menu_callback(self, menu_item):
		"""
		Callback function for the Help menu item. This is not used in
		fallback mode.
		"""
		Gtk.show_uri(None, "".join(("help:", PROGRAM_NAME.lower())), Gdk.CURRENT_TIME)

	def preferences_button_callback(self, button, window):
		"""
		Callback function for the Preferences button on the toolbar in
		fallback mode. This is not used in normal mode.
		"""
		preferences_window = PreferencesWindow(
			parent=window,
			settings=self.settings,
			color_settings=self.color_settings)
		self.get_application().add_window(preferences_window)
		preferences_window.show_all()

	def preferences_menu_callback(self, menu_item, window):
		"""
		Callback function for the Preferences menu item. This is not
		used in fallback mode.
		"""
		preferences_window = PreferencesWindow(
			parent=window,
			settings=self.settings,
			color_settings=self.color_settings)
		self.get_application().add_window(preferences_window)
		preferences_window.show_all()

	def settings_changed(self, settings, key, menu_item):
		"""
		Callback function for GSettings.
		"""
		if key == "show-calendar-key":
			menu_item.set_active(settings.get_boolean(key))

	def show_calendar_key_menu_callback(self, menu_item):
		"""
		Callback function for Show Calendar Key menu item. This is not
		used in fallback mode.
		"""
		state = menu_item.get_active()
		self.settings.set_boolean("show-calendar-key", state)

	def stack_callback(self, stack, gparam_spect):
		"""
		Callback function for stack switcher. This is not used in
		fallback mode.
		"""
		if stack.props.visible_child_name == "calendar":
			self.new_note_button.set_visible(False)
			self.calendar_navigation.set_visible(True)
		elif stack.props.visible_child_name == "notes":
			self.calendar_navigation.set_visible(False)
			self.new_note_button.set_visible(True)
		else:
			self.calendar_navigation.set_visible(False)
			self.new_note_button.set_visible(False)


class NoteWindow(Gtk.ApplicationWindow):
	"""
	Window for viewing, editing, or removing a note journal entry.
	"""
	def __init__(self, parent=None, journal=None, journal_entry=None, date=None):
		Gtk.ApplicationWindow.__init__(self,
			default_height=200,
			default_width=300,
			border_width=12,
			destroy_with_parent=True)
		self.journal=journal
		self.journal_entry = journal_entry
		if date:
			self.date = date
		elif journal_entry:
			self.date = journal_entry.get_date()
		else:
			self.date = None

		if self.journal_entry:
			self.set_title(_("Edit Note"))
			text = self.journal_entry.get_note()
		else:
			self.set_title(_("Add Note"))
			text = ""
		self.set_transient_for(parent)
		self.set_type_hint(Gdk.WindowTypeHint.DIALOG)

		self.calendar = Gtk.Calendar()
		if self.date:
			self.calendar.select_day(0)
			self.calendar.select_month(int(self.date.get_month()) - 1, self.date.get_year())
			self.calendar.select_day(self.date.get_day())

		self.text_buffer = Gtk.TextBuffer(text=text)
		self.text_view = Gtk.TextView.new_with_buffer(self.text_buffer)
		self.text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)

		scrolled_window = Gtk.ScrolledWindow()
		scrolled_window.add(self.text_view)

		ok_button = Gtk.Button.new_from_stock(Gtk.STOCK_SAVE)
		ok_button.connect("clicked", self.clicked, Gtk.STOCK_SAVE)
		cancel_button = Gtk.Button.new_from_stock(Gtk.STOCK_CANCEL)
		cancel_button.connect("clicked", self.clicked, Gtk.STOCK_CANCEL)

		# if editing existing note
		if self.journal_entry:
			remove_button = Gtk.Button.new_from_stock(Gtk.STOCK_REMOVE)
			remove_button.connect("clicked", self.clicked, Gtk.STOCK_REMOVE)

		buttons_box = Gtk.Box(spacing=12, homogeneous=True)
		buttons_box.pack_end(ok_button, True, True, 0)
		buttons_box.pack_end(cancel_button, True, True, 0)
		if self.journal_entry:
			buttons_box.pack_end(remove_button, True, True, 0)

		vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
		#vbox.pack_start(self.calendar, False, False, 0)
		vbox.pack_start(scrolled_window, True, True, 0)
		vbox.pack_start(buttons_box, False, False, 0)
		self.add(vbox)

	def clicked(self, button, button_type):
		if button_type == Gtk.STOCK_REMOVE:
			self.journal.remove_entry(self.journal_entry)
		elif button_type == Gtk.STOCK_SAVE:
			start, end = self.text_buffer.get_bounds()
			text = self.text_buffer.get_text(start, end, False)
			year, zero_based_month, day = self.calendar.get_date()
			month = zero_based_month + 1
			self.date = new_date(day, month, year)
			if len(text) > 0:
				if self.journal_entry:
					self.journal.remove_entry(self.journal_entry)
				self.journal.new_entry(self.date, JournalEntryTypeKnown.NOTE, text)
			elif len(text) == 0 and self.journal_entry:
				self.journal.remove_entry(self.journal_entry)
		self.destroy()


class NotesView(Gtk.ScrolledWindow):
	"""
	NotesView offers an alternative way to view, enter, and edit note
	journal entries. It is meant to be embedded in MainWindow.
	"""
	def __init__(self, journal):
		Gtk.ScrolledWindow.__init__(self)
		# Gtk.ListBox requires GTK+ 3.10
		try:
			self.list_box = Gtk.ListBox()
		except AttributeError:
			self.list_box = None

		if self.list_box is not None:
			self.add(self.list_box)
			self.list_box.set_sort_func(self.sort, None)
			self.journal = journal
			self.journal.connect("add-entry", self.refresh)
			self.journal.connect("remove-entry", self.refresh)
			self.refresh(self.journal)
		self.show_all()

	def new_note(self):
		"""
		Initalize an instance of NoteWindow in order for user to create
		a new note journal entry.
		"""
		parent = self.get_toplevel()
		note_window = NoteWindow(parent=parent, journal=self.journal)
		parent.get_application().add_window(note_window)
		note_window.show_all()

	def refresh(self, journal):
		"""
		Callback function used to refresh the list of notes	whenever a
		journal entry has been added or removed from the journal.
		"""
		if self.list_box is None:
			return
		for child in self.list_box.get_children():
			child.destroy()
		for journal_entry in self.journal.get_all_entries(journal_entry_type=JournalEntryTypeKnown.NOTE):
			#year, month, day = journal_entry.get_ymd()
			date_label = Gtk.Label.new(journal_entry.get_key())
			note_label = Gtk.Label.new(journal_entry.get_note())
			note_label.set_halign(Gtk.Align.START)
			note_label.set_line_wrap(True)

			hbox = Gtk.Box(spacing=12)
			hbox.pack_start(date_label, False, False, 0)
			hbox.pack_start(note_label, True, True, 0)

			event_box = Gtk.EventBox()
			event_box.connect("button-press-event", self.row_clicked, journal_entry)
			event_box.add(hbox)
			event_box.show_all()
			self.list_box.add(event_box)

	def row_clicked(self, event_box, event, journal_entry):
		"""
		Callback function for clicking on a row in the list of note
		journal entries.
		"""
		if event.button == 3:
			parent = self.get_toplevel()
			notes_view_popup = NotesViewPopup(parent, self.journal, journal_entry)
			notes_view_popup.popup_for_device(event.device, None, None, None, None, event.button, event.time)

	def sort(self, row1, row2, data):
		"""
		Sort function for the list box.
		"""
		eventbox1 = row1.get_child()
		eventbox2 = row2.get_child()
		key1 = eventbox1.get_child().get_children()[0].get_text()
		key2 = eventbox2.get_child().get_children()[0].get_text()
		year1, month1, day1 = key1.split("-")
		year2, month2, day2 = key2.split("-")

		newest_first = -1
		oldest_first = 1
		sort_direction = oldest_first

		if year1 != year2:
			if year1 > year2:
				return sort_direction
			else:
				return sort_direction * -1

		if month1 != month2:
			if month1 > month2:
				return sort_direction
			else:
				return sort_direction * -1

		if day1 != day2:
			if day1 > day2:
				return sort_direction
			else:
				return sort_direction * -1

		return 0


class NotesViewPopup(Gtk.Menu):
	"""
	Popup menu used by NotesView when a row is clicked.
	"""
	def __init__(self, parent, journal, journal_entry):
		"""
		parent: GtkWindow instance
		journal: Journal instance
		journal_entry: JournalEntry instance
		"""
		Gtk.Menu.__init__(self)
		self.journal = journal
		self.parent = parent

		menu_item = Gtk.MenuItem(label=_("Edit Note"))
		menu_item.connect("activate", self.activate, journal_entry)
		self.add(menu_item)
		self.show_all()

	def activate(self, menu_item, journal_entry):
		"""
		Callback function for each menu item.
		"""
		# get the main window
		note_window = NoteWindow(
			parent=self.parent,
			journal=self.journal,
			journal_entry=journal_entry)
		self.parent.get_application().add_window(note_window)
		note_window.show_all()


class PasswordDialog(Gtk.Dialog):
	"""
	Dialog window used to prompt user to enter a password. This is used
	for both setting a new password as well as entering a previously
	chosen password in order to decrypt a journal that is locked.
	"""
	def __init__(self, parent, title, button1, button2):
		"""
		parent: GtkWindow instance
		title: string to use for dialog window title
		button1: string to use for the label of the first button
		button2: string to use for the label of the second button
		"""
		Gtk.Dialog.__init__(
			self,
			title=title,
			border_width=12,
			transient_for=parent)

		self.password_entry = Gtk.Entry(visibility=False)
		self.password_entry.set_activates_default(True)
		vbox = self.get_content_area()
		vbox.set_spacing(12)
		vbox.add(self.password_entry)

		self.add_buttons(button1, Gtk.ResponseType.CANCEL, button2, Gtk.ResponseType.OK)
		self.set_default_response(Gtk.ResponseType.OK)

	def get_password(self):
		"""
		Returns the contents of the password entry widget.

		Returns: string
		"""
		return self.password_entry.get_text()


class PreferencesWindow(Gtk.ApplicationWindow):
	"""
	Window to allow user to change program settings.

	It provides a normal mode for GTK+ 3.x versions >= 3.10 and a
	fallback mode for GTK+ 3.x versions that are less than 3.10.
	"""
	def __init__(self, parent=None, settings=None, color_settings=None):
		"""
		parent: GtkWindow instance
		settings: GSettings instance
		color_settings: Gsettings instance
		"""
		Gtk.ApplicationWindow.__init__(
			self,
			border_width=12,
			destroy_with_parent=True,
			title=_("Preferences"),
			window_position=Gtk.WindowPosition.CENTER)
		self.set_transient_for(parent)
		self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
		self.settings = settings
		self.color_settings = color_settings
		self.settings.connect("changed", self.settings_changed)
		self.color_settings.connect("changed", self.color_settings_changed)
		self.journal = parent.journal

		self.calendar_key = CalendarKey(color_settings)

		lower_limit_setting = self.settings.get_int("menstrual-cycle-lower-limit")
		upper_limit_setting = self.settings.get_int("menstrual-cycle-upper-limit")
		cols_setting = self.settings.get_int("calendar-view-columns")
		rows_setting = self.settings.get_int("calendar-view-rows")
		method = self.settings.get_enum("fertility-awareness-method")

		duration_setting =  self.settings.get_int("birth-control-duration")
		skip_setting = self.settings.get_int("birth-control-inactive-days")
		show_key_setting = self.settings.get_boolean("show-calendar-key")

		grid_widgets = []
		grid = Gtk.Grid(column_spacing=12, row_spacing=12)

		general_label = Gtk.Label.new("".join(("<b>", _("General"), "</b>")))
		general_label.set_use_markup(True)
		general_label.set_halign(Gtk.Align.START)
		grid_widgets.append((general_label, None))

		if not self.journal.is_locked() and self.journal.has_crypto_support():
			can_change_password_state = True
		else:
			can_change_password_state = False

		self.require_password_label = Gtk.Label.new(_("Require Password"))
		self.require_password_label.set_sensitive(can_change_password_state)

		self.password_checkbutton = Gtk.CheckButton()
		self.password_checkbutton.set_active(self.journal.is_encrypted())
		self.password_checkbutton.set_sensitive(can_change_password_state)
		self.password_checkbutton.connect("toggled", self.encryption_toggled, self.journal)
		grid_widgets.append((self.require_password_label, self.password_checkbutton))

		password_label = Gtk.Label.new(_("Password"))
		password_label.set_sensitive(False)

		password_entry = Gtk.Entry(visibility=False)
		password_entry.set_sensitive(False)
		#grid_widgets.append((password_label, password_entry))

		calendar_label = Gtk.Label.new("".join(("<b>", _("Calendar"), "</b>")))
		calendar_label.set_use_markup(True)
		grid_widgets.append((calendar_label, None))

		show_key_label = Gtk.Label.new(_("Show Key"))
		self.show_key_checkbutton = Gtk.CheckButton(active=show_key_setting)
		self.show_key_checkbutton.connect("toggled", self.button_toggled, "show-calendar-key")
		grid_widgets.append((show_key_label, self.show_key_checkbutton))

		cols_label = Gtk.Label.new(_("Columns"))
		self.cols_sb = Gtk.SpinButton.new_with_range(1, 4, 1)
		self.cols_sb.set_value(cols_setting)
		self.cols_sb.connect("value-changed", self.spin_button_changed, "calendar-view-columns")
		grid_widgets.append((cols_label, self.cols_sb))

		rows_label = Gtk.Label.new(_("Rows"))
		self.rows_sb = Gtk.SpinButton.new_with_range(1, 3, 1)
		self.rows_sb.set_value(rows_setting)
		self.rows_sb.connect("value-changed", self.spin_button_changed, "calendar-view-rows")
		grid_widgets.append((rows_label, self.rows_sb))

		menstrual_cycles_label = Gtk.Label.new("".join(("<b>", _("Menstrual Cycle"), "</b>")))
		menstrual_cycles_label.set_use_markup(True)
		grid_widgets.append((menstrual_cycles_label, None))

		method_label = Gtk.Label.new(_("Fertility Awareness Method"))

		self.method_combo_box = Gtk.ComboBoxText()
		self.method_combo_box.append(str(FertilityAwarenessMethod.STANDARD_DAYS), _("Standard Days"))
		self.method_combo_box.append(str(FertilityAwarenessMethod.RHYTHM), _("Rhythm (Knaus–Ogino)"))
		self.method_combo_box.set_active_id(str(method))
		self.method_combo_box.connect("changed", self.combo_box_changed, "fertility-awareness-method")
		grid_widgets.append((method_label, self.method_combo_box))

		lower_limit_label = Gtk.Label.new(_("Ignore Cycles Shorter Than"))

		self.lower_limit_sb = Gtk.SpinButton.new_with_range(0, 28, 1)
		self.lower_limit_sb.set_value(lower_limit_setting)
		self.lower_limit_sb.connect("value-changed", self.spin_button_changed, "menstrual-cycle-lower-limit")
		days_label1 = Gtk.Label.new(_("Days"))
		lower_limit_box = Gtk.Box(spacing = 6)
		lower_limit_box.pack_start(self.lower_limit_sb, False, False, 0)
		lower_limit_box.pack_start(days_label1, False, False, 0)
		grid_widgets.append((lower_limit_label, lower_limit_box))

		upper_limit_label = Gtk.Label.new(_("Ignore Cycles Longer Than"))

		self.upper_limit_sb = Gtk.SpinButton.new_with_range(28, 100, 1)
		self.upper_limit_sb.set_value(upper_limit_setting)
		self.upper_limit_sb.connect("value-changed", self.spin_button_changed, "menstrual-cycle-upper-limit")
		days_label = Gtk.Label.new(_("Days"))
		upper_limit_box = Gtk.Box(spacing = 6)
		upper_limit_box.pack_start(self.upper_limit_sb, False, False, 0)
		upper_limit_box.pack_start(days_label, False, False, 0)
		grid_widgets.append((upper_limit_label, upper_limit_box))

		birth_control_label = Gtk.Label.new("".join(("<b>", _("Birth Control"), "</b>")))
		birth_control_label.set_use_markup(True)
		grid_widgets.append((birth_control_label, None))

		duration_label = Gtk.Label.new(_("Duration"))

		self.duration_sb = Gtk.SpinButton.new_with_range(28, 366, 1)
		self.duration_sb.set_value(duration_setting)
		self.duration_sb.connect("value-changed", self.spin_button_changed, "birth-control-duration")
		days_label = Gtk.Label.new(_("Days"))
		duration_box = Gtk.Box(spacing = 6)
		duration_box.pack_start(self.duration_sb, False, False, 0)
		duration_box.pack_start(days_label, False, False, 0)
		grid_widgets.append((duration_label, duration_box))

		inactive_days_label = Gtk.Label.new(_("Placebo (or Pause)"))

		self.skip_sb = Gtk.SpinButton.new_with_range(0, 7, 1)
		self.skip_sb.set_value(skip_setting)
		self.skip_sb.connect("value-changed", self.spin_button_changed, "birth-control-inactive-days")
		days_label = Gtk.Label.new(_("Days"))
		skip_box = Gtk.Box(spacing = 6)
		skip_box.pack_start(self.skip_sb, False, False, 0)
		skip_box.pack_start(days_label, False, False, 0)
		grid_widgets.append((inactive_days_label, skip_box))

		self.reset_settings_button = Gtk.Button.new_with_label(_("Reset to Defaults"))
		self.reset_settings_button.set_valign(Gtk.Align.END)
		self.reset_settings_button.connect("clicked", self.reset_settings)
		self.set_reset_settings_button_state()

		reset_settings_box = Gtk.Box()
		reset_settings_box.pack_end(self.reset_settings_button, False, False, 0)

		# populate grid
		for row in range(len(grid_widgets)):
			column1, column2 = grid_widgets[row]
			if isinstance(column1, Gtk.Label):
				column1.set_halign(Gtk.Align.START)
				column1.set_valign(Gtk.Align.CENTER)

			if column2:
				# set_margin_start requires GTK+ 3.12
				try:
					column1.set_margin_start(18)
				except AttributeError:
					column1.set_margin_left(18)
				column2.set_halign(Gtk.Align.START)
				column2.set_valign(Gtk.Align.CENTER)

				grid.attach(column1, 0, row, 1, 1)
				grid.attach(column2, 1, row, 1, 1)
			else:
				grid.attach(column1, 0, row, 2, 1)

		settings_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
		settings_vbox.pack_start(grid, True, True, 0)
		settings_vbox.pack_start(reset_settings_box, False, False, 0)

		### color settings ###

		color_settings_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
		calendar_key = CalendarKey(self.color_settings)
		color_settings_vbox.pack_start(calendar_key, True, True, 0)

		self.reset_colors_button = Gtk.Button.new_with_label(_("Reset to Defaults"))
		self.reset_colors_button.set_valign(Gtk.Align.END)
		self.reset_colors_button.connect("clicked", self.reset_colors)
		self.set_reset_colors_button_state()
		reset_colors_box = Gtk.Box()
		reset_colors_box.pack_end(self.reset_colors_button, False, False, 0)
		color_settings_vbox.pack_start(reset_colors_box, False, False, 0)

		# Gtk.Stack requires GTK+ 3.10
		try:
			stack = Gtk.Stack()
			notebook = None
		except AttributeError:
			notebook = Gtk.Notebook()
			stack = None

		if stack is not None:
			headerbar = Gtk.HeaderBar(show_close_button=True, title=_("Preferences"))
			stack.add_titled(settings_vbox, "general-settings", _("General"))
			stack.add_titled(color_settings_vbox, "color-settings", _("Colors"))
			switcher = Gtk.StackSwitcher()
			switcher.set_stack(stack)
			headerbar.set_custom_title(switcher)
			self.set_titlebar(headerbar)
			self.add(stack)

		# fallback mode for GTK+ versions < 3.10
		else:
			settings_vbox.set_border_width(12)
			color_settings_vbox.set_border_width(12)
			notebook.append_page(settings_vbox, Gtk.Label.new(_("General")))
			notebook.append_page(color_settings_vbox, Gtk.Label.new(_("Colors")))
			self.add(notebook)

	def button_toggled(self, button, key):
		"""
		Callback function for Show Calendar Key checkbox.
		"""
		self.settings.set_boolean(key, button.get_active())

	def color_settings_changed(self, color_settings, color_settings_key):
		"""
		Callback function for GSettings.
		"""
		self.set_reset_colors_button_state()

	def combo_box_changed(self, combo_box, key):
		"""
		Callback function for prediction method combobox.
		"""
		self.settings.set_enum(key, int(combo_box.get_active_id()))

	def encryption_toggled(self, password_checkbutton, journal):
		"""
		Callback function for Use Password checkbox.
		"""
		if password_checkbutton.get_active():
			application = self.get_application()
			password_dialog = PasswordDialog(self, _("Choose a Password"), _("Cancel"), _("Set Password"))
			application.add_window(password_dialog)
			password_dialog.show_all()
			response = password_dialog.run()
			if response == Gtk.ResponseType.OK:
				journal.set_password(password_dialog.get_password())
			else:
				password_checkbutton.set_active(False)
			password_dialog.destroy()

		else:
			journal.set_password(None)

		self.set_reset_settings_button_state()

	def reset_colors(self, button):
		"""
		Callback function for Reset to Defaults button (for colors
		settings).
		"""
		for key in self.color_settings.keys():
			# get_user_value requires GLib 2.40
			try:
				value = self.color_settings.get_user_value(key)
				if value is not None:
					self.color_settings.reset(key)
					assert self.color_settings_get_user_value(key) is None
			except AttributeError:
				self.color_settings.reset(key)

	def reset_settings(self, reset_settings_button):
		"""
		Callback function for Reset to Defaults button (for non-color
		settings).
		"""
		for key in self.settings.keys():
			# get_user_value requires GLib 2.40
			try:
				value = self.settings.get_user_value(key)
				if value is not None:
					self.settings.reset(key)
					assert self.settings_get_user_value(key) is None
			except AttributeError:
				self.settings.reset(key)
		if self.journal.is_locked() is False:
			self.password_checkbutton.set_active(False)

	def set_reset_colors_button_state(self):
		"""
		If it is possible to tell that the the user has not changed the
		default color settings, set the Result to Defaults button to
		insensitive. Otherwise, set it to sensitive.
		"""
		for key in self.color_settings.keys():
			# get_user_value requires GLib 2.40
			try:
				value = self.color_settings.get_user_value(key)
				if value is not None:
					self.reset_colors_button.set_sensitive(True)
					return
			except AttributeError:
				self.reset_colors_button.set_sensitive(True)
				return

		self.reset_colors_button.set_sensitive(False)

	def set_reset_settings_button_state(self):
		"""
		If it is possible to tell that the the user has not changed the
		default non-color settings, set the Result to Defaults button to
		insensitive. Otherwise, set it to sensitive.
		"""
		if self.password_checkbutton.get_active():
			self.reset_settings_button.set_sensitive(True)
			return
		else:
			for key in self.settings.keys():
				# get_user_value requires GLib 2.40
				try:
					value = self.settings.get_user_value(key)
					if value is not None:
						self.reset_settings_button.set_sensitive(True)
						return
				except AttributeError:
					self.reset_settings_button.set_sensitive(True)
					return

		self.reset_settings_button.set_sensitive(False)

	def settings_changed(self, settings, settings_key):
		"""
		Callback function for Gsettings.
		"""
		if settings_key == "calendar-view-columns":
			cols_setting = self.settings.get_int(settings_key)
			self.cols_sb.set_value(cols_setting)
		elif settings_key == "calendar-view-rows":
			rows_setting = self.settings.get_int(settings_key)
			self.rows_sb.set_value(rows_setting)
		elif settings_key == "fertility-awareness-method":
			method = self.settings.get_enum(settings_key)
			self.method_combo_box.set_active(method)
		elif settings_key == "menstrual-cycle-lower-limit":
			lower_limit = self.settings.get_int(settings_key)
			self.lower_limit_sb.set_value(lower_limit)
		elif settings_key == "menstrual-cycle-upper-limit":
			upper_limit = self.settings.get_int(settings_key)
			self.upper_limit_sb.set_value(upper_limit)
		elif settings_key == "birth-control-duration":
			duration = self.settings.get_int(settings_key)
			self.duration_sb.set_value(duration)
		elif settings_key == "birth-control-inactive-days":
			skip = self.settings.get_int(settings_key)
			self.skip_sb.set_value(skip)
		elif settings_key == "show-calendar-key":
			state = self.settings.get_boolean(settings_key)
			self.show_key_checkbutton.set_active(state)

		self.set_reset_settings_button_state()

	def spin_button_changed(self, spin_button, key):
		"""
		Callback function for cycle upper limit and cycle lower limit
		spin buttons.
		"""
		self.settings.set_int(key, spin_button.get_value())


class StatisticsView(Gtk.Grid):
	"""
	StatisticsView displays various statistics about the journal and the
	entries it contains. It is meant to be embedded in MainWindow.
	"""
	def __init__(self, journal):
		"""
		journal: Journal instance
		"""
		Gtk.Grid.__init__(
			self,
			column_spacing=6,
			row_spacing=12,
			column_homogeneous=False)
		self.journal = journal
		self.journal.connect("stats-updated", self.refresh)

		menstrual_cycles_label = Gtk.Label.new("".join(("<b>", _("Menstrual Cycles"), "</b>")))
		menstrual_cycles_label.set_use_markup(True)
		menstrual_cycles_label.set_halign(Gtk.Align.CENTER)
		self.attach(menstrual_cycles_label, 0, 0, 2, 1)

		statistics_names = [_("Entries Logged:"),
						  _("Average Duration:"),
						 _("Shortest Duration:"),
						  _("Longest Duration:")]
		for label_number in range(len(statistics_names)):
			name_label = Gtk.Label.new(statistics_names[label_number])
			name_label.set_halign(Gtk.Align.END)
			# set_margin_start requires GTK+ 3.12
			try:
				name_label.set_margin_start(18)
			except AttributeError:
				name_label.set_margin_left(18)
			self.attach(name_label, 0, label_number+1, 1, 1)

			value_label = Gtk.Label.new("")
			value_label.set_halign(Gtk.Align.START)
			self.attach(value_label, 1, label_number+1, 1, 1)
		self.refresh(self.journal)
		self.show_all()

	def refresh(self, journal):
		"""
		Callback function used to refresh the statistics displayed
		whenever the journal updates its statistics.
		"""
		total = self.journal.get_total_entries(JournalEntryTypeKnown.CYCLE)
		shortest = self.journal.get_shortest_cycle_length()
		average = self.journal.get_average_cycle_length()
		longest = self.journal.get_longest_cycle_length()
		# Gtk.Grid.get_child_at missing from some Python bindings
		try:
			self.get_child_at(1, 1).set_text(str(total) if total else "")
			self.get_child_at(1, 2).set_text("".join((str(average), " ", _("days"), " ")) if average else "")
			self.get_child_at(1, 3).set_text("".join((str(shortest), " ", _("days"), " (", _("Ended"), " ", self.journal.get_shortest_cycle_entry().get_key(), ")")) if shortest else "")
			self.get_child_at(1, 4).set_text("".join((str(longest), " ", _("days"), " (", _("Ended"), " ", self.journal.get_longest_cycle_entry().get_key(), ")")) if longest else "")
		except AttributeError:
			pass


# work around for the poor error handling in GLib.Date
def new_date(day, month, year):
	for number in (day, month, year):
		if not isinstance(number, int):
			raise EizelleError
	if (65535 >= year >= 1 and
		12 >= month >= 1 and
		31 >= day >= 1 and
		GLib.Date.valid_dmy(day, month, year) is True):
		date = GLib.Date.new_dmy(day, month, year)
	else:
		raise EizelleError

	return date


def print_version():
	print(" ".join([PROGRAM_NAME.lower(), "version", VERSION_NUMBER]))


if __name__ == '__main__':
	if "--help" in sys.argv:
		if "--version" in sys.argv:
			print_version()
		print ("\nUsage:")
		print ("".join(("  ", PROGRAM_NAME.lower(), " [OPTION]...")))
		print ("\nOptions:")
		print (" {0:20} {1}".format("--help", "display this help and exit"))
		print (" {0:20} {1}".format("--version", "display the version number and exit"))
		sys.exit(0)

	elif "--version" in sys.argv:
		print_version()
		sys.exit(0)

	else:
		application = Application()
		sys.exit(application.run(sys.argv))
